Skip to content

Add Salesforce chat widget to book details page#2873

Open
OpenStaxClaude wants to merge 12 commits into
mainfrom
add-chat-to-book-details
Open

Add Salesforce chat widget to book details page#2873
OpenStaxClaude wants to merge 12 commits into
mainfrom
add-chat-to-book-details

Conversation

@OpenStaxClaude
Copy link
Copy Markdown
Contributor

@OpenStaxClaude OpenStaxClaude commented May 22, 2026

Summary

Adds Salesforce Embedded Messaging chat widget with flexible feature flag control. The chat widget persists across navigation to maintain conversation state and can be enabled for:

  • Specific pages (book details, subjects, contact)
  • All pages for logged-in users

Related to: https://openstax.atlassian.net/browse/CORE-1416

Changes

  • Added multiple feature flags to shared-data.ts:
    • chat_book_details - Enable chat on book details pages
    • chat_subjects - Enable chat on subjects pages
    • chat_contact - Enable chat on contact page
    • chat_logged_in - Enable chat site-wide for logged-in users
  • Created new Chat component at src/app/components/chat/
    • Dynamically loads Salesforce bootstrap script
    • Initializes with org ID 00DU0000000Kwch and deployment Web_Messaging_Deployment
    • Passes user information to pre-chat form when available:
      • First name, last name, email, school (for logged-in users)
      • OpenStax UUID (hidden field)
    • Configures sProduct field as "Website" for all users
    • Uses window flag to prevent re-initialization across component remounts
    • Handles cleanup on unmount by hiding widget (preserving session state)
  • Integrated Chat component into router at MainRoutes level
    • Renders conditionally based on feature flags and current route
    • Positioned as fixed overlay in lower right corner
    • Persists across navigation to maintain conversation state
    • Shows chat based on any of these conditions:
      • User is logged in AND chat_logged_in flag is enabled
      • On book details page AND chat_book_details flag is enabled
      • On subjects page AND chat_subjects flag is enabled
      • On contact page AND chat_contact flag is enabled
  • Added comprehensive test coverage for Chat component
  • Centralized isLoggedIn logic in useUserContext for consistency

Implementation Details

Following the pattern from the Accounts repository, adapted for React/Preact architecture.

Feature Flags

The chat widget is controlled by multiple CMS feature flags that need to be created at https://openstax.org/django-admin/api/featureflag/:

  • chat_book_details: Enable chat on book details pages (/details/*)
  • chat_subjects: Enable chat on subjects pages (/subjects/*)
  • chat_contact: Enable chat on contact page (/contact)
  • chat_logged_in: Enable chat site-wide for logged-in users (any page)

Flags are additive - if multiple flags are enabled, chat will show on all applicable pages.

User Experience

  • Chat button appears in lower right corner when feature flags are enabled
  • For logged-in users: pre-chat form is pre-populated with user information
  • For anonymous users: sProduct is still set to "Website"
  • Widget persists across navigation between chat-enabled pages to maintain conversation state
  • Widget is hidden (via display: none) when navigating away from chat-enabled pages
    • This preserves the conversation session while preventing the widget from appearing on non-enabled pages
    • When returning to a chat-enabled page, the widget reappears with the conversation intact

Technical Implementation

  • Component mounted at router level (in MainRoutes) rather than page level
  • Uses window.__salesforceChatInitialized flag to track initialization globally
  • Derives stable user primitives from userContext.userStatus to avoid unnecessary effect triggers
  • Preserves window.embeddedservice_bootstrap object on component remount to maintain session
  • Hides/shows Salesforce-injected DOM elements on unmount/mount to control visibility
  • Clears script event handlers on cleanup to prevent setState on unmounted component warnings

Testing

  • Added unit tests for Chat component
  • Test with logged-in user (faculty and student)
  • Test with anonymous user
  • Verify chat button positioning across different viewport sizes
  • Test navigation between different pages maintains conversation
  • Test all feature flag combinations
  • Verify widget hides when navigating to non-enabled pages
  • Verify widget reappears when returning to enabled pages

Review Comments Addressed

  • ✅ Fixed chat initialization to use window flag and stable primitives to prevent re-initialization
  • ✅ Set sProduct for all users (not just logged-in users)
  • ✅ Added all user information fields (FirstName, LastName, Email, School) to pre-chat form
  • ✅ Added comprehensive test coverage
  • ✅ Moved Chat component to router level for persistence across navigation
  • ✅ Added multiple feature flags for flexible chat placement
  • ✅ Fixed cleanup logic to always hide widget on unmount
  • ✅ Short-circuit script loading when bootstrap already exists
  • ✅ Centralized isLoggedIn logic in useUserContext
  • ✅ Clear script handlers to prevent setState warnings on unmounted component

🤖 Generated with Claude Code

@RoyEJohnson RoyEJohnson marked this pull request as ready for review May 22, 2026 19:10
@RoyEJohnson RoyEJohnson marked this pull request as draft May 22, 2026 19:10
@mintlify

This comment was marked as outdated.

@RoyEJohnson RoyEJohnson force-pushed the add-chat-to-book-details branch from 9669126 to 30d6cc0 Compare May 26, 2026 17:45
@RoyEJohnson RoyEJohnson marked this pull request as ready for review May 26, 2026 18:23
@RoyEJohnson RoyEJohnson requested review from Copilot and mwvolo May 26, 2026 18:23
@mintlify

This comment was marked as outdated.

This comment was marked as resolved.

@RoyEJohnson RoyEJohnson marked this pull request as draft May 26, 2026 18:29
RoyEJohnson

This comment was marked as duplicate.

RoyEJohnson

This comment was marked as resolved.

@OpenStaxClaude

This comment was marked as resolved.

OpenStaxClaude and others added 2 commits May 26, 2026 14:03
- Add chat_book_details feature flag to shared-data.ts
- Create Chat component with Salesforce Embedded Messaging integration
- Integrate Chat component into BookDetails page with feature flag
- Pass user information to Salesforce pre-chat form when available
- Configure sProduct field as "Website" for Salesforce

Related to CORE-1416

🤖 Generated with [Claude Code](https://claude.com/claude-code)

lint

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
- Fix chat initialization to prevent re-initialization on userContext changes
  - Use ref to track initialization state
  - Derive stable user primitives to avoid unnecessary effect triggers
  - Only initialize once even if user context object changes

- Set sProduct field for all users (logged in and anonymous)
  - Previously only set for logged-in users
  - Now always set to "Website"

- Add all user information fields to pre-chat form
  - FirstName, LastName, Email, School in addition to UUID
  - Only set fields that are available

- Add comprehensive tests for Chat component
  - Test script injection and initialization
  - Test user field handling for various scenarios
  - Test cleanup behavior
  - Test single initialization guarantee

- Move Chat component to router level for persistence
  - Chat now renders at MainRoutes level in router.tsx
  - Maintains conversation state across navigation
  - Only shown on book details pages when feature flag enabled
  - Removed from BookDetails component

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Replace empty CSS rule with comment

Lint

Co-Authored-By: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

This comment was marked as low quality.

This comment was marked as low quality.

@RoyEJohnson RoyEJohnson marked this pull request as ready for review May 26, 2026 21:32
@mintlify
Copy link
Copy Markdown

mintlify Bot commented May 26, 2026

Docs PR opened: https://github.com/mintlify-community/docs-openstax-16c06674/pull/6

Added a book details chat widget section and documented the chat_book_details feature flag in the flags API reference.

RoyEJohnson

This comment was marked as resolved.

@OpenStaxClaude

This comment was marked as resolved.

@OpenStaxClaude

This comment was marked as resolved.

- Add explicit hide/show logic for Salesforce chat widget
  - Hide chat elements on component unmount to prevent widget from staying visible
  - Show chat elements on component mount if widget was previously initialized
  - Targets Salesforce-injected DOM elements (.embeddedServiceHelpButton, etc.)
  - Ensures route/flag gating works correctly after initialization

- Remove setTimeout from unit test
  - Test can assert immediately since no new onload event is triggered
  - Prevents test suite slowdown and potential flakiness
  - __salesforceChatInitialized flag prevents re-init without delay

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Shorten lines

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
@RoyEJohnson RoyEJohnson force-pushed the add-chat-to-book-details branch from 8090398 to e525469 Compare May 27, 2026 16:12
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 5 out of 5 changed files in this pull request and generated 3 comments.

Comment thread src/app/components/chat/chat.tsx
Comment thread src/app/components/chat/chat.tsx
Comment thread test/src/components/chat.test.tsx Outdated
Copy link
Copy Markdown
Member

@mwvolo mwvolo left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure if these copilot review comments are of concern.
It seemed like I was experiencing loading issues when I was testing it after hours, which I didn't see in-hours. The widget hides itself outside business hours, so I'm not sure if that could case some issues. When I went to a subjects page (tried in a couple browsers), nothing was loading.
Probably worth a little closer inspection.

@OpenStaxClaude

This comment was marked as resolved.

This comment was marked as resolved.

RoyEJohnson

This comment was marked as resolved.

@OpenStaxClaude

This comment was marked as resolved.

OpenStaxClaude and others added 2 commits June 2, 2026 10:22
Comment 1: Short-circuit script loading when bootstrap already exists
- Check if window.embeddedservice_bootstrap exists on mount
- If it exists, immediately set scriptLoaded=true and skip script injection
- Prevents unnecessary script reload and speeds up widget re-display on remount
- Eliminates potential delay or failure when re-showing hidden widget

Comment 2: Only set initialization flag on successful init
- Changed initEmbeddedMessaging() to return boolean success indicator
- Only set window.__salesforceChatInitialized = true if init succeeded
- Prevents permanently broken state if initialization fails
- Allows retry on future mounts if initialization threw an error

Comment 3: Update test to reflect new script loading behavior
- Updated test comment and assertions to match short-circuit logic
- Verifies no new script is injected on remount when bootstrap exists
- Added explicit check that script count remains 0 after remount
- Clarifies that scriptLoaded is set immediately via short-circuit

Performance optimization in router.tsx:
- Extract isLoggedIn as stable primitive before useMemo
- Only re-evaluate showChat when isLoggedIn changes, not entire userContext
- Reduces unnecessary re-renders when unrelated user properties update

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Consistent return value

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
**Issue:** `useUserContext()` only exposes `userModel` when `model.last_name`
is truthy, which means logged-in users without a last name would be treated
as anonymous and pre-chat fields wouldn't populate.

**Resolution:**
- Changed to derive user primitives from `userStatus` (always available)
- Added fallback to `userModel` for compatibility
- User info now extracted using: `userStatus?.uuid || userModel?.uuid`
- Same pattern applied for firstName, lastName, email, school

**Issue:** When short-circuiting because `window.embeddedservice_bootstrap`
already exists, the effect returned a no-op cleanup (`() => undefined`),
so the hide-on-unmount logic never ran, leaving the widget visible.

**Resolution:**
- Refactored to always return proper cleanup function
- Moved script creation into conditional block
- Cleanup now always hides chat widget on unmount, regardless of
  whether script was injected or short-circuited

**Issue:** Using `userModel?.uuid` to determine login state can incorrectly
treat some logged-in users as anonymous (same root cause as comment 1).

**Resolution:**
- Changed to: `userStatus?.uuid || userModel?.uuid`
- Ensures `chat_logged_in` flag works correctly for all logged-in users
- Site-wide chat now properly enabled when flag is set

**Before:**
- User info only available when `last_name` was present
- Short-circuit path had no cleanup logic
- Some logged-in users treated as anonymous

**After:**
- User info always accessible via `userStatus`
- Cleanup always runs to hide widget on unmount
- All logged-in users correctly identified

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Update router.tsx

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
@RoyEJohnson RoyEJohnson force-pushed the add-chat-to-book-details branch from 203b28a to a813084 Compare June 2, 2026 15:22
RoyEJohnson

This comment was marked as resolved.

@OpenStaxClaude

This comment was marked as resolved.

Per Roy's review comment, consolidate isLoggedIn logic into a single
canonical location in contexts/user.ts and have all other places use it.

Changes:
- Added isLoggedIn to useUserContext return value
  - Uses userStatus?.uuid || model?.uuid (the correct check)
  - Always available in context, even when userModel is not
  - Documented as the canonical way to check login state

- Updated all places that previously defined isLoggedIn locally:
  1. router.tsx - Removed local calculation, now uses context
  2. form-header.tsx - Removed incorrect last_name check, now uses context
  3. adoption.tsx - Removed incorrect last_name checks (2 instances), now uses context

Benefits:
- Single source of truth for login state
- Fixes inconsistent implementations (some used last_name incorrectly)
- Ensures all features work correctly for users without last names
- Easier to maintain and update in the future

Addresses Review 15 feedback.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Update adoption.tsx

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

This comment was marked as resolved.

RoyEJohnson

This comment was marked as resolved.

1. Clear script handlers on cleanup (Comment 1)
   - Clear onload and onerror handlers before removing script
   - Prevents setState-on-unmounted-component warnings
   - Handlers can fire after unmount if network request completes late

2. Update PR description (Comments 2 & 3)
   - Clarified that widget is hidden (not removed) to preserve session state
   - Added explicit description of hide/show behavior when navigating
   - Updated User Experience section with widget visibility details
   - Updated Technical Implementation section

3. Add uuid to user context fallback branch (Comment 4)
   - Expose uuid in fallback branch: {isLoggedIn, userStatus, myOpenStaxUser, uuid: userStatus?.uuid}
   - Makes context shape consistent across both branches
   - Consumers can now reliably destructure uuid alongside isLoggedIn
   - uuid will be undefined for truly anonymous users

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
@OpenStaxClaude
Copy link
Copy Markdown
Contributor Author

I've addressed all 4 unresolved Copilot review comments from Review 16. Here's a summary of the changes:

Changes Made

1. Clear script handlers on cleanup (Comment 1)

Issue: The cleanup function removed the <script> element but left onload and onerror handlers attached. If the network request completed after unmount, the handler could still fire and call setScriptLoaded(true), leading to setState-on-unmounted-component warnings.

Resolution:

  • Added script.onload = null and script.onerror = null before removing the script
  • Prevents any late-firing handlers from attempting setState after component unmounts
  • Eliminates potential React warnings and unnecessary work

2. Update PR description to clarify hiding behavior (Comments 2 & 3)

Issue:

  • Comment 2: PR description said widget is "removed from DOM" but code only hides it with display: none
  • Comment 3 (Roy): Confirmed hiding is correct because we want state to persist

Resolution:

  • Updated PR description to clarify widget is hidden (not removed) to preserve session state
  • Added detailed explanation of hide/show behavior:
    • Widget hidden via display: none when navigating away from enabled pages
    • Widget reappears when returning to enabled pages with conversation intact
  • Updated both User Experience and Technical Implementation sections
  • Aligned documentation with actual implementation and intent

3. Add uuid to user context fallback branch (Comment 4)

Issue: useUserContext exposes isLoggedIn derived from uuid, but the fallback branch (when model.last_name is falsy) didn't expose uuid. This made the context shape inconsistent - it could report isLoggedIn=true while uuid was undefined for consumers destructuring both fields.

Resolution:

  • Added uuid: userStatus?.uuid to fallback branch
  • Context now returns consistent shape in both branches:
    • Full branch: {accountId, userName, userModel, uuid, isVerified, isLoggedIn, userStatus, ...}
    • Fallback branch: {isLoggedIn, userStatus, myOpenStaxUser, uuid}
  • Consumers can now reliably destructure uuid alongside isLoggedIn
  • uuid will be undefined for truly anonymous users (expected behavior)

Technical Summary

Root causes:

  1. Missing handler cleanup led to potential React warnings
  2. Documentation didn't match implementation intent
  3. Inconsistent object shapes across context branches

All fixes committed and pushed. The implementation now:

  • ✅ Prevents setState warnings on unmounted components
  • ✅ Has accurate documentation matching behavior
  • ✅ Returns consistent context shape for all consumers

Ready for final review! @RoyEJohnson

@RoyEJohnson RoyEJohnson requested a review from mwvolo June 2, 2026 17:48
@RoyEJohnson
Copy link
Copy Markdown
Contributor

The latest version is deployed to Dev (tag is add-chat-button-d4)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants